import Link from "next/link"; import { notFound } from "next/navigation"; import { EbookManifestSchema } from "@/lib/schemas/ebook"; import { PublishedCatalogSchema } from "@/lib/schemas/published-book"; import ReadingListButton from "./ReadingListButton"; export const revalidate = 60; const ACCENT_HERO: Record = { amber: "from-amber-950/90 via-slate-950/60 to-slate-950", cyan: "from-cyan-950/90 via-slate-950/60 to-slate-950", emerald: "from-emerald-950/90 via-slate-950/60 to-slate-950", rose: "from-rose-950/90 via-slate-950/60 to-slate-950", violet: "from-violet-950/90 via-slate-950/60 to-slate-950", slate: "from-slate-800/90 via-slate-950/60 to-slate-950", }; const ACCENT_TEXT: Record = { amber: "text-amber-400", cyan: "text-cyan-400", emerald: "text-emerald-400", rose: "text-rose-400", violet: "text-violet-400", slate: "text-slate-400", }; const ACCENT_BG: Record = { amber: "bg-amber-500 hover:bg-amber-400", cyan: "bg-cyan-500 hover:bg-cyan-400", emerald: "bg-emerald-500 hover:bg-emerald-400", rose: "bg-rose-500 hover:bg-rose-400", violet: "bg-violet-500 hover:bg-violet-400", slate: "bg-slate-500 hover:bg-slate-400", }; const ACCENT_BORDER: Record = { amber: "border-amber-500/30 text-amber-300 bg-amber-500/10", cyan: "border-cyan-500/30 text-cyan-300 bg-cyan-500/10", emerald: "border-emerald-500/30 text-emerald-300 bg-emerald-500/10", rose: "border-rose-500/30 text-rose-300 bg-rose-500/10", violet: "border-violet-500/30 text-violet-300 bg-violet-500/10", slate: "border-slate-500/30 text-slate-300 bg-slate-500/10", }; const ACCENT_COVER_GRAD: Record = { amber: "from-amber-950 via-amber-900 to-amber-800", cyan: "from-cyan-950 via-cyan-900 to-cyan-800", emerald: "from-emerald-950 via-emerald-900 to-emerald-800", rose: "from-rose-950 via-rose-900 to-rose-800", violet: "from-violet-950 via-violet-900 to-violet-800", slate: "from-slate-900 via-slate-800 to-slate-700", }; const ACCENT_AVATAR: Record = { amber: "bg-amber-500/15 text-amber-300 ring-amber-500/30", cyan: "bg-cyan-500/15 text-cyan-300 ring-cyan-500/30", emerald: "bg-emerald-500/15 text-emerald-300 ring-emerald-500/30", rose: "bg-rose-500/15 text-rose-300 ring-rose-500/30", violet: "bg-violet-500/15 text-violet-300 ring-violet-500/30", slate: "bg-slate-500/15 text-slate-300 ring-slate-500/30", }; // ── Data fetchers ───────────────────────────────────────────────────────────── async function fetchManifest(slug: string) { const pub = process.env.R2_PUBLIC_URL; if (!pub) return null; try { const res = await fetch( `${pub.replace(/\/$/, "")}/published/${slug}/manifest.json`, { next: { revalidate: 60 } }, ); if (!res.ok) return null; const parsed = EbookManifestSchema.safeParse(await res.json()); return parsed.success ? parsed.data : null; } catch { return null; } } async function fetchAccent(slug: string): Promise { const pub = process.env.R2_PUBLIC_URL; if (!pub) return "amber"; try { const res = await fetch( `${pub.replace(/\/$/, "")}/published/index.json`, { next: { revalidate: 60 } }, ); if (!res.ok) return "amber"; const parsed = PublishedCatalogSchema.safeParse(await res.json()); return parsed.success ? (parsed.data.books.find((b) => b.slug === slug)?.coverAccent ?? "amber") : "amber"; } catch { return "amber"; } } // ── Page ────────────────────────────────────────────────────────────────────── export default async function BookLandingPage({ params, }: { params: Promise<{ slug: string }>; }) { const { slug } = await params; const [manifest, accent] = await Promise.all([ fetchManifest(slug), fetchAccent(slug), ]); if (!manifest) notFound(); const heroGrad = ACCENT_HERO[accent] ?? ACCENT_HERO.amber; const accentText = ACCENT_TEXT[accent] ?? ACCENT_TEXT.amber; const accentBg = ACCENT_BG[accent] ?? ACCENT_BG.amber; const accentBdr = ACCENT_BORDER[accent] ?? ACCENT_BORDER.amber; const coverGrad = ACCENT_COVER_GRAD[accent] ?? ACCENT_COVER_GRAD.amber; const avatarCls = ACCENT_AVATAR[accent] ?? ACCENT_AVATAR.amber; const totalMins = Math.ceil(manifest.totalWordCount / 200); const readTime = totalMins >= 60 ? `~${Math.round(totalMins / 60)}h` : `~${totalMins} min`; return (
{/* ── Hero gradient band ─────────────────────────────────────────────── */}
{/* Nav */}
Library
{/* Two-column hero: cover mockup + text */}
{/* Book cover mockup or real image */}
{manifest.coverImageUrl ? ( // eslint-disable-next-line @next/next/no-img-element {`${manifest.bookTitle} ) : ( <>

{manifest.authorName}

{manifest.bookTitle}

)}
{/* Title + meta + CTA */}

{manifest.authorName}

{manifest.bookTitle}

{manifest.subtitle && (

{manifest.subtitle}

)} {/* Stats pills */}
{manifest.chapters.length} chapters {manifest.totalWordCount.toLocaleString()} words {readTime} read
Start Reading
{/* ── Body content ──────────────────────────────────────────────────────── */}
{/* ── Left: intro + chapter list ──────────────────────────────────── */}
{manifest.frontMatter.introduction && (

Introduction

{manifest.frontMatter.introduction.replace(/#{1,6} /g, "").trim()}

)}

Contents

    {manifest.chapters.map((ch, i) => (
  1. {ch.number} {ch.title} {ch.totalWordCount > 0 && ( {Math.ceil(ch.totalWordCount / 200)} min )}
  2. ))}
{/* ── Right sidebar ───────────────────────────────────────────────── */}
{/* Author bio card */} {manifest.frontMatter.aboutAuthor && (
{manifest.authorImageUrl ? ( // eslint-disable-next-line @next/next/no-img-element {manifest.authorName} ) : (
{manifest.authorName.charAt(0).toUpperCase()}
)}

{manifest.authorName}

Author

{manifest.frontMatter.aboutAuthor}

)}
); }